﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using log4net;
using Reactor.Environment;
using Reactor.Messaging.Attributes;
using Samurai.Wakizashi.Concurrency;

namespace Reactor.Messaging.ServiceBus
{
    public class DefaultHandlerRegistry : IHandlerRegistry
    {
        #region Fields

        private static readonly ILog Log = LogManager.GetLogger(typeof(IHandlerRegistry));
        private readonly ReaderWriterLock _readerWriterLock = new ReaderWriterLock();
        private readonly List<MessageHandlerRegistration> _messageHandlerRegistrations = new List<MessageHandlerRegistration>();
        private readonly IList<MessageHandlerRegistration> _newRegistrations = new List<MessageHandlerRegistration>();

        #endregion

        #region Implementation of IHandlerRegistry

        public event EventHandler<HandlersRegisteredEventArgs> HandlersRegistered;

        /// <summary>
        /// Compiles a list of all assemblies loaded into the current AppDomain, removes predetermined
        /// assemblies known to not contain message handlers, and forwards the final list to the ScanAssembliesForHandlers
        /// overload that takes an enumerable list of assemblies.
        /// </summary>
        public void ScanAssembliesForHandlers()
        {
            var referencedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            var excluded = new[] {"System", "mscorlib", "Microsoft.VisualStudio.HostingProcess.Utilities", "System.Windows.Forms", "System.Drawing", "Microsoft.VisualStudio.HostingProcess.Utilities.Sync", "Microsoft.VisualStudio.Debugger.Runtime", "vshost32", "System.Data", "System.Core", "System.Xml", "Microsoft.CSharp", "System.Xml.Linq", "System.Data.DataSetExtensions", "System.Xaml", "WindowsBase", "PresentationCore", "PresentationFramework", "System.Configuration", "log4net", "PresentationFramework.Aero", "Reactor.ServiceLocator.Unity", "Microsoft.Practices.Unity", "Microsoft.Practices.Unity.ServiceLocatorAdapter", "Microsoft.Practices.ServiceLocation", "Apache.NMS.ActiveMQ", "Apache.NMS", "YamlSerializer", "System.Transactions", "System.EnterpriseServices"};

            var assembliesToScan = (from referencedAssembly in referencedAssemblies
                                    where !string.IsNullOrEmpty(referencedAssembly.FullName)
                                    let name = referencedAssembly.FullName.Split(',')[0]
                                    where !excluded.Contains(name)
                                    select referencedAssembly).ToList();

            ScanAssembliesForHandlers(assembliesToScan);
        }

        /// <summary>
        /// Scans the assemblies for handlers.
        /// </summary>
        /// <param name="assemblies">The assemblies.</param>
        public void ScanAssembliesForHandlers(IEnumerable<Assembly> assemblies)
        {
            foreach (var handlerType in GetHandlerTypesContainedInAssemblies(assemblies))
                RegisterHandlerType(handlerType);

            MessageHandlerRegistration[] newRegistrations = null;
            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(10)))
            {
                if (_newRegistrations.Count > 0)
                    newRegistrations = _newRegistrations.ToArray();

                _newRegistrations.Clear();
            }

            if (HandlersRegistered != null && newRegistrations != null && newRegistrations.Length > 0)
                HandlersRegistered(this, new HandlersRegisteredEventArgs { Registrations = newRegistrations });
        }

        /// <summary>
        /// Manually registers the type of the message handler. This method is useful 
        /// for handlers that have dynamic destinations bound to it at runtime, rather 
        /// than static subscription declarations in the form of attributes.
        /// </summary>
        /// <param name="messageHandlerType">Type of the message handler.</param>
        /// <param name="expectedDestinations">The expected destinations.</param>
        public void RegisterMessageHandlerType(Type messageHandlerType, IList<IMessagingChannel> expectedDestinations)
        {
            var messageTypesHandled = messageHandlerType.GetAllPossibleMessageTypesHandled();
            if (!messageTypesHandled.Any()) return;

            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(10)))
            {
                // See if the handler type is already registered. Do not continue if so.
                var handlerRegistration = _messageHandlerRegistrations.SingleOrDefault(r => r.MessageHandlerType == messageHandlerType);
                if (handlerRegistration != null) return;

                // Create handler registration
                handlerRegistration = new MessageHandlerRegistration
                {
                    MessageHandlerType = messageHandlerType,
                    // NOTE: The decision was made to only support one msg type per handling type
                    MessageType = messageHandlerType.GetDeclaredMessageTypeHandledByConcreteType(),   
                    ExpectedDestinations = expectedDestinations
                };
                _messageHandlerRegistrations.Add(handlerRegistration);
                _newRegistrations.Add(handlerRegistration);
            }
        }

        /// <summary>
        /// Gets all message handler registrations.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<MessageHandlerRegistration> GetAllHandlerRegistrations()
        {
            using (new ReaderLock(_readerWriterLock, TimeSpan.FromSeconds(15)))
            {
                return _messageHandlerRegistrations.ToArray();
            }
        }

        /// <summary>
        /// Gets all registered handler types that handle the specified message type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public IList<Type> GetHandlerTypesForMessageType<T>() where T : class
        {
            return GetHandlerTypesForMessageType(typeof (T));
        }

        /// <summary>
        /// Gets all types that handle the specified message type.
        /// </summary>
        /// <param name="messageType">Message Type</param>
        /// <returns>List of types that handle the specified message type.</returns>
        public IList<Type> GetHandlerTypesForMessageType(Type messageType)
        {
            var handlerTypes = new HashSet<Type>();
            using (new ReaderLock(_readerWriterLock, TimeSpan.FromSeconds(10)))
            {
                foreach (var reg in _messageHandlerRegistrations)
                {
                    var match = reg.MessageHandlerType.CanAcceptMessageType(messageType);
                    if (match)
                        handlerTypes.Add(reg.MessageHandlerType);
                }
            }
            return handlerTypes.ToList();
        }

        /// <summary>
        /// Gets all handler types contained in the provided assemblies.
        /// </summary>
        /// <param name="assemblies">The assemblies.</param>
        /// <returns></returns>
        public IList<Type> GetHandlerTypesContainedInAssemblies(IEnumerable<Assembly> assemblies)
        {
            var handlerTypes = new List<Type>();
            foreach (var assembly in assemblies)
            {
                Type[] types;
                try
                {
                    types = assembly.GetTypes();
                }
                catch (ReflectionTypeLoadException e)
                {
                    Log.Error(string.Format("Unable to load types from assembly: {0}. First loader exception: {1}", assembly.FullName, e.LoaderExceptions[0].Message), e);
                    continue;
                }

                handlerTypes.AddRange(types.Where(type => type.IsMessageHandler()));
            }

            return handlerTypes;
        }

        #endregion

        private void RegisterHandlerType(Type messageHandlerType)
        {
            var destinations = GetDestinationsExpectedForHandler(messageHandlerType);
            RegisterMessageHandlerType(messageHandlerType, destinations);
        }

        private static List<IMessagingChannel> GetDestinationsExpectedForHandler(Type messageHandlerType)
        {
            var list = new List<IMessagingChannel>();

            // Seek out queue attributes and create destinations for each
            var queueAttributes = messageHandlerType.GetCustomAttributes(typeof (PointToPointChannelAttribute), true);
            list.AddRange(from PointToPointChannelAttribute attribute in queueAttributes
                          select new PointToPointChannel(attribute.Name) { IsTransactional = attribute.IsTransactional });

            // If any queues have no Name value, assume they are the current service's default queue, which is named the service's name
            foreach (var queue in list.Where(q => string.IsNullOrEmpty(q.DestinationName)))
                queue.DestinationName = Context.ServiceBus.Configuration.CurrentEndpointInputChannel.DestinationName;

            // Seek out topic attributes and create destinations for each
            var topicAttributes = messageHandlerType.GetCustomAttributes(typeof (PubSubChannelAttribute), true);
            foreach (PubSubChannelAttribute topicAttribute in topicAttributes)
            {
                if (string.IsNullOrEmpty(topicAttribute.TopicName))
                {
                    var messageTypes = messageHandlerType.GetAllPossibleMessageTypesHandled();
                    list.AddRange(from Type t in messageTypes
                                  select new PubSubChannel(t.FullName) {IsTransactional = true});
                }
                else
                    list.Add(new PubSubChannel(topicAttribute.TopicName, topicAttribute.IsTransactional)
                                 {IsTransactional = topicAttribute.IsTransactional});
            }

            return list;
        }
    }
}
